Skip to content

port vehicle system - #1477

Merged
Rxup merged 18 commits into
masterfrom
feature/vehicle-system
Jul 3, 2026
Merged

port vehicle system#1477
Rxup merged 18 commits into
masterfrom
feature/vehicle-system

Conversation

@Rxup

@Rxup Rxup commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Добавлен новый интерфейс для управления руками меха и отображения их в хотбаре.
    • Появились новые типы транспорта, ключей и связанные элементы интерфейса.
    • В меню паузы и экране выхода добавлен пункт спонсора.
  • Bug Fixes

    • Улучшена работа взаимодействий с мехами, транспортом и ремнями.
    • Исправлено отображение глубины при пристёгивании.
    • Обновлены подсказки и локализация для новых элементов интерфейса.

@github-actions github-actions Bot added size/L S: Untriaged Changes: Sprites Should be reviewed or fixed by people who are knowledgeable with spriting or visual design. Changes: Localization Can be reviewed or fixed by people who are knowledgeable with translation and removed S: Untriaged size/L labels Jul 3, 2026
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

RSI Diff Bot; head commit c94a326 merging into 56de3e3
This PR makes changes to 1 or more RSIs. Here is a summary of all changes:

Resources/Textures/Objects/Vehicles/janicart.rsi

State Old New Status
vehicle Modified
vehicle-moving Added

Resources/Textures/Objects/Vehicles/wheelchair.rsi

State Old New Status
wheelchair_overlay Added

Edit: diff updated after c94a326

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@Rxup, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 35 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 750d096d-9465-4b6e-9412-70125a7861c1

📥 Commits

Reviewing files that changed from the base of the PR and between 84fa3a2 and c94a326.

📒 Files selected for processing (1)
  • Content.Client/Backmen/VovaMech/BkmVovaMechHandsUIController.cs

Walkthrough

Реализована абстракция транспорта (VehicleSystem) с компонентами Operator/Strap/Container/GenericKeyed, на которую переведены Mech, Buckle, Mover, DoAfter и CardboardBox. Добавлена подсистема BkmVovaMech (сервер, shared, клиент с UI рук меха). Обновлены прототипы транспорта, локализация, миграции; кнопка спонсора перенесена из главного меню в меню паузы.

Changes

Транспортная система и миграция

Layer / File(s) Summary
Компоненты транспорта
Content.Shared/Vehicle/Components/*, Content.Shared/Movement/Events/UpdateCanMoveEvent.cs
Введены VehicleComponent, VehicleOperatorComponent, ContainerVehicleComponent, GenericKeyedVehicleComponent, StrapVehicleComponent, VehicleVisuals и события CanMoveUpdatedEvent/EffectiveMoverChangedEvent.
VehicleSystem: оператор, ключи, страп
Content.Shared/Vehicle/VehicleSystem*.cs, Content.Shared/ActionBlocker/ActionBlockerSystem.cs
Реализованы TrySetOperator/TryRemoveOperator/CanOperate/RefreshCanRun, трансфер урона оператору, логика ключевых контейнеров и страпа; ActionBlockerSystem рассылает CanMoveUpdatedEvent.
Миграция Mech на VehicleOperator
Content.Shared/Mech/..., Content.Server/Mech/Systems/*, Content.Shared/Mech/Components/MechComponent.cs
Mech-системы переведены с MechPilotComponent/PilotSlot на VehicleOperatorComponent; удалены устаревшие поля MechComponent и файл MechPilotComponent.
Mover/DoAfter: effective mover
Content.Shared/Movement/Systems/*, Content.Server/Physics/Controllers/MoverController.cs, Content.Shared/DoAfter/*
SharedMoverController вычисляет effective mover и рассылает изменения; DoAfter хранит MovementEntity, отменяется при смене effective mover.
Buckle/CardboardBox интеграция
Content.Shared/Buckle/..., Content.Client/Buckle/BuckleSystem.cs, Content.*/CardboardBox/*
Buckle учитывает VehicleOperatorComponent/ModifyBuckleDrawDepth; CardboardBox получает оператора через VehicleSystem.
BkmVovaMech (shared/server)
Content.Shared/Backmen/VovaMech/*, Content.Server/Backmen/VovaMech/BkmVovaMechSystem.cs, Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs
Добавлены компонент пилотируемого меха, вербы входа/выхода, смена активной руки и определение "холдера" оружия для стрельбы.
Прототипы, локализация, миграции
Resources/Prototypes/**, Resources/Locale/**, Resources/migration.yml, Resources/Textures/Objects/Vehicles/**
Добавлены VehicleWheelchair/VehicleJanicart/VehicleKeyJanicart, обновлены мехи/боксы, тег JanicartKeys, локализация и миграции прототипов.

Estimated code review effort: 5 (Critical) | ~150 minutes

Клиентский UI рук пилотируемого меха

Layer / File(s) Summary
Клиентское отслеживание меха
Content.Client/Backmen/VovaMech/BkmVovaMechSystem.cs
Публикует LocalPilotedMechChanged при смене оператора для локального игрока.
UI строки рук и обработка кликов
Content.Client/Backmen/VovaMech/BkmVovaMechHandsUIController.cs, Content.Client/UserInterface/Systems/Hotbar/Widgets/HotbarGui.xaml, локализация
Строка кнопок рук меха с кулдаунами, выбором активной руки и обработкой использования предметов.

Estimated code review effort: 4 (Complex) | ~60 minutes

Кнопка спонсора

Layer / File(s) Summary
Перенос SponsorButton
Content.Client/MainMenu/UI/*, Content.Client/Options/UI/EscapeMenu.xaml, Content.Client/UserInterface/Systems/EscapeMenu/EscapeUIController.cs, локализация
Кнопка удалена из главного меню и добавлена в меню паузы с той же IoC-логикой открытия.

Estimated code review effort: 2 (Simple) | ~10 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Player
  participant VehicleSystem
  participant Operator as VehicleOperatorComponent
  participant BkmVovaMechSystem
  Player->>VehicleSystem: TrySetOperator(vehicle, user)
  VehicleSystem->>Operator: установить Vehicle
  VehicleSystem->>BkmVovaMechSystem: VehicleOperatorSetEvent
  BkmVovaMechSystem->>BkmVovaMechSystem: EnsureActiveMechHand
  VehicleSystem->>VehicleSystem: RefreshCanRun
Loading
sequenceDiagram
  participant ClientBkmVovaMechSystem
  participant BkmVovaMechHandsUIController
  participant HandsSystem
  ClientBkmVovaMechSystem->>BkmVovaMechHandsUIController: LocalPilotedMechChanged(mech)
  BkmVovaMechHandsUIController->>BkmVovaMechHandsUIController: LoadMechHands
  BkmVovaMechHandsUIController->>HandsSystem: MechHandClick / SetActiveHand
  HandsSystem-->>BkmVovaMechHandsUIController: обновление кнопки
Loading

Poem

Мех шагает, руки в ряд,
Оператор занял слот подряд,
DoAfter следит за шагом чутко,
Спонсор в паузу вернулся шуткой. 🐇
Хрум-хрум, коммит готов, вперёд!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Заголовок кратко и точно отражает основное изменение: перенос/внедрение системы транспорта.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/vehicle-system

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the size/L label Jul 3, 2026
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

This pull request has conflicts, please resolve those before we can evaluate the pull request.

@github-actions github-actions Bot added the Changes: UI Can be reviewed or fixed by people who are knowledgeable with UI design label Jul 3, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (5)
Content.Shared/Vehicle/VehicleSystem.Key.cs (1)

28-48: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Дублирование логики между OnGenericKeyedEntInserted и OnGenericKeyedEntRemoved.

Оба обработчика идентичны за исключением имени: проверяют _timing.ApplyingState, ID контейнера, резолвят VehicleComponent и вызывают RefreshCanRun. Можно вынести общую логику в приватный метод.

♻️ Пример рефакторинга
-    private void OnGenericKeyedEntInserted(Entity<GenericKeyedVehicleComponent> ent, ref EntInsertedIntoContainerMessage args)
-    {
-        if (_timing.ApplyingState || args.Container.ID != ent.Comp.ContainerId)
-            return;
-
-        if (!_vehicleQuery.TryComp(ent, out var vehicle))
-            return;
-
-        RefreshCanRun((ent.Owner, vehicle));
-    }
-
-    private void OnGenericKeyedEntRemoved(Entity<GenericKeyedVehicleComponent> ent, ref EntRemovedFromContainerMessage args)
-    {
-        if (_timing.ApplyingState || args.Container.ID != ent.Comp.ContainerId)
-            return;
-
-        if (!_vehicleQuery.TryComp(ent, out var vehicle))
-            return;
-
-        RefreshCanRun((ent.Owner, vehicle));
-    }
+    private void OnGenericKeyedEntInserted(Entity<GenericKeyedVehicleComponent> ent, ref EntInsertedIntoContainerMessage args)
+    {
+        if (args.Container.ID == ent.Comp.ContainerId)
+            TryRefreshKeyedCanRun(ent);
+    }
+
+    private void OnGenericKeyedEntRemoved(Entity<GenericKeyedVehicleComponent> ent, ref EntRemovedFromContainerMessage args)
+    {
+        if (args.Container.ID == ent.Comp.ContainerId)
+            TryRefreshKeyedCanRun(ent);
+    }
+
+    private void TryRefreshKeyedCanRun(Entity<GenericKeyedVehicleComponent> ent)
+    {
+        if (_timing.ApplyingState || !_vehicleQuery.TryComp(ent, out var vehicle))
+            return;
+
+        RefreshCanRun((ent.Owner, vehicle));
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Content.Shared/Vehicle/VehicleSystem.Key.cs` around lines 28 - 48,
`OnGenericKeyedEntInserted` and `OnGenericKeyedEntRemoved` in
`VehicleSystem.Key` duplicate the same state checks, container ID guard, vehicle
lookup, and `RefreshCanRun` call; extract this shared flow into a private helper
and have both handlers delegate to it. Keep the helper centered around the
existing `Entity<GenericKeyedVehicleComponent>` and `RefreshCanRun` path so the
behavior remains identical while removing the repeated logic.
Content.Shared/Backmen/VovaMech/SharedBkmVovaMechSystem.cs (2)

154-167: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Дублирование логики старта DoAfter входа.

Этот блок почти идентичен StartEntryDoAfter в Content.Server/Backmen/VovaMech/BkmVovaMechSystem.cs (строки 106-114). Стоит вынести общий метод (например, protected void StartEntryDoAfter(EntityUid uid, BkmPilotableMechComponent component, EntityUid user)) в этот shared-класс и переиспользовать его из OnDragDrop и из серверных verb-обработчиков, чтобы избежать расхождения параметров DoAfterArgs (например, BreakOnMove) в будущем.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Content.Shared/Backmen/VovaMech/SharedBkmVovaMechSystem.cs` around lines 154
- 167, The DoAfter startup logic for mech entry is duplicated between OnDragDrop
and StartEntryDoAfter, so move the shared setup into a common helper in
BkmVovaMechSystem (for example, a protected StartEntryDoAfter method taking
EntityUid uid, BkmPilotableMechComponent component, and the user/dragged
entity). Have both OnDragDrop and the server verb path call that helper so the
DoAfterArgs construction stays consistent, including flags like BreakOnMove and
the entry event type.

17-21: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Сделать общие зависимости protected вместо private.

_actionBlocker и _doAfter объявлены private, из-за чего серверный BkmVovaMechSystem вынужден повторно инжектировать те же системы отдельными полями (см. Content.Server/Backmen/VovaMech/BkmVovaMechSystem.cs, строки 19-23). Это дублирование DI и потенциальный источник рассинхронизации при будущих изменениях.

♻️ Предлагаемый рефакторинг
-    [Dependency] private ActionBlockerSystem _actionBlocker = default!;
+    [Dependency] protected ActionBlockerSystem ActionBlocker = default!;
     [Dependency] private SharedContainerSystem _container = default!;
-    [Dependency] private SharedDoAfterSystem _doAfter = default!;
+    [Dependency] protected SharedDoAfterSystem DoAfter = default!;
     [Dependency] private SharedHandsSystem _hands = default!;
     [Dependency] protected VehicleSystem Vehicle = default!;

и убрать дублирующие поля _actionBlocker/_doAfter/_vehicle в серверном классе, заменив их на ActionBlocker/DoAfter/Vehicle.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Content.Shared/Backmen/VovaMech/SharedBkmVovaMechSystem.cs` around lines 17 -
21, Make the shared dependencies in SharedBkmVovaMechSystem protected instead of
private so the server-side BkmVovaMechSystem can reuse them directly rather than
reinjecting the same systems. Update the dependency fields for _actionBlocker
and _doAfter in SharedBkmVovaMechSystem, then remove the duplicated
ActionBlocker/DoAfter/_vehicle fields from BkmVovaMechSystem and switch its
usage to the inherited ActionBlocker, DoAfter, and Vehicle members.
Content.Shared/Mech/EntitySystems/SharedMechSystem.cs (1)

67-69: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Обработчики атаки/оружия для VehicleOperatorComponent расположены в mech-специфичной системе.

OnGetMeleeWeapon, OnCanAttackFromContainer, OnAttackAttempt подписаны на общий VehicleOperatorComponent, то есть будут срабатывать для ЛЮБОГО транспорта (инвалидное кресло, джаникарт и т.д.), а не только для меха. Логика внутри их защищена проверками TryComp<MechComponent>/_pilotableMechQuery, поэтому функционально не ломается, но с точки зрения границ модулей это создаёт неявную зависимость всех транспортных средств от SharedMechSystem. Целесообразнее вынести общую часть (AttackAttemptEvent/CanAttackFromContainerEvent cancel-логику "нельзя атаковать собственный транспорт") в VehicleSystem, оставив в SharedMechSystem только mech-специфичный GetMeleeWeaponEvent fallback.

Also applies to: 393-432

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Content.Shared/Mech/EntitySystems/SharedMechSystem.cs` around lines 67 - 69,
The attack/weapon event subscriptions on VehicleOperatorComponent are too broad
for SharedMechSystem and should be split by responsibility. Move the generic
cancel logic for AttackAttemptEvent and CanAttackFromContainerEvent into
VehicleSystem, keep only the mech-specific GetMeleeWeaponEvent fallback in
SharedMechSystem, and preserve the existing
TryComp<MechComponent>/_pilotableMechQuery checks while relocating the handlers
OnGetMeleeWeapon, OnCanAttackFromContainer, and OnAttackAttempt to the
appropriate system.
Content.Server/Mech/Systems/MechSystem.cs (1)

314-314: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Уберите дублирующий _actionBlocker.UpdateCanMove в MechSystem. Vehicle.RefreshCanRun(uid) уже вызывает _actionBlocker.UpdateCanMove(uid) и обновляет appearance, поэтому после InsertBattery/RemoveBattery этот вызов лишний. В оставшихся прямых обработчиках (OnInteractUsing, OnInsertBattery, OnRemoveBattery, OnMapInit) лучше перейти на Vehicle.RefreshCanRun(uid), чтобы поведение не расходилось.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Content.Server/Mech/Systems/MechSystem.cs` at line 314, Remove the redundant
direct `_actionBlocker.UpdateCanMove` path in `MechSystem`:
`Vehicle.RefreshCanRun(uid)` already performs the move-state update and
appearance refresh, so the extra call after `InsertBattery`/`RemoveBattery`
should be dropped. Update the remaining direct handlers in `MechSystem`
(`OnInteractUsing`, `OnInsertBattery`, `OnRemoveBattery`, `OnMapInit`) to
consistently call `Vehicle.RefreshCanRun(uid)` instead of duplicating
`_actionBlocker.UpdateCanMove`, keeping the behavior centralized and aligned.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Content.Client/Backmen/VovaMech/BkmVovaMechHandsUIController.cs`:
- Around line 92-112: `_mechHands` can stay null when `HandsComponent` is added
after `LocalPilotedMechChanged`, so the mech hands UI never initializes in the
current session. Update `OnMechHandAdded` in `BkmVovaMechHandsUIController` to
recover from this late-start case by reloading hands when the added component
belongs to `_mechUid` and `_mechHands` is still null, or add a dedicated startup
hook for `HandsComponent` that calls `LoadMechHands`. Keep the existing
`AddHandButton` and `SetActiveHand` flow intact once hands are restored.

In `@Content.Server/Backmen/VovaMech/BkmVovaMechSystem.cs`:
- Around line 144-148: The OnEntry flow in BkmVovaMechSystem currently returns
silently when TryInsert(uid, args.User, component) fails, unlike the earlier
mind/CanOperate checks that show "mech-no-enter". Update the TryInsert failure
branch to send the same popup/feedback to the user before returning, keeping the
UX consistent in OnEntry and preserving the existing args.Handled behavior only
on success.

In `@Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs`:
- Line 483: The buckle release check in SharedBuckleSystem.Buckle should
preserve SoftCritical behavior, since _mobState.IsIncapacitated(buckle) only
covers hard crit/dead and drops soft crit. Update the condition around
HasComp<KnockedDownComponent> and the incapacitation check so it matches the
previous IsCritical || IsDead semantics, keeping SoftCritical entities lying
down when unbuckled.

In `@Content.Shared/Vehicle/VehicleSystem.cs`:
- Around line 113-189: In TrySetOperator, when removeExisting is true and uid
already has a VehicleOperatorComponent for another vehicle, clear that previous
vehicle’s operator state before reassigning. Update VehicleSystem.TrySetOperator
so it removes the old operator from the previous VehicleComponent, refreshes
that vehicle’s state (including operator/can-run related flags), and only then
assigns uid to the new entity. Use the existing operator handling paths around
_operatorQuery, VehicleOperatorComponent.Vehicle, and RefreshCanRun to keep both
vehicles consistent.

In `@Content.Shared/Vehicle/VehicleSystem.Operator.cs`:
- Around line 18-30: Добавьте проверку _timing.ApplyingState в обработчики
OnVehicleStrapped и OnVehicleUnstrapped, чтобы они вели себя согласованно с
OnContainerEntInserted/OnContainerEntRemoved и обработчиками в
VehicleSystem.Key.cs. Сейчас эти события сразу вызывают TrySetOperator, что
может повторно срабатывать при применении клиентского состояния; перед вызовом
TrySetOperator просто выходите из обработчика, если _timing.ApplyingState
истинно.

In `@Resources/Prototypes/Entities/Objects/Specific/Mech/mechs.yml`:
- Around line 232-248: `MechHamtr` still relies on the default
`VehicleSystem.CanOperate` hand requirement, which blocks `MobHamster` from
piloting because it has no `HandsComponent`. Update the `MechHamtr` entity
definition in the mech YAML to explicitly set `requiresHands` to false on the
`Vehicle` component, keeping the existing `operatorWhitelist` for `Hamster`
intact so the mech can be operated without hands.

---

Nitpick comments:
In `@Content.Server/Mech/Systems/MechSystem.cs`:
- Line 314: Remove the redundant direct `_actionBlocker.UpdateCanMove` path in
`MechSystem`: `Vehicle.RefreshCanRun(uid)` already performs the move-state
update and appearance refresh, so the extra call after
`InsertBattery`/`RemoveBattery` should be dropped. Update the remaining direct
handlers in `MechSystem` (`OnInteractUsing`, `OnInsertBattery`,
`OnRemoveBattery`, `OnMapInit`) to consistently call
`Vehicle.RefreshCanRun(uid)` instead of duplicating
`_actionBlocker.UpdateCanMove`, keeping the behavior centralized and aligned.

In `@Content.Shared/Backmen/VovaMech/SharedBkmVovaMechSystem.cs`:
- Around line 154-167: The DoAfter startup logic for mech entry is duplicated
between OnDragDrop and StartEntryDoAfter, so move the shared setup into a common
helper in BkmVovaMechSystem (for example, a protected StartEntryDoAfter method
taking EntityUid uid, BkmPilotableMechComponent component, and the user/dragged
entity). Have both OnDragDrop and the server verb path call that helper so the
DoAfterArgs construction stays consistent, including flags like BreakOnMove and
the entry event type.
- Around line 17-21: Make the shared dependencies in SharedBkmVovaMechSystem
protected instead of private so the server-side BkmVovaMechSystem can reuse them
directly rather than reinjecting the same systems. Update the dependency fields
for _actionBlocker and _doAfter in SharedBkmVovaMechSystem, then remove the
duplicated ActionBlocker/DoAfter/_vehicle fields from BkmVovaMechSystem and
switch its usage to the inherited ActionBlocker, DoAfter, and Vehicle members.

In `@Content.Shared/Mech/EntitySystems/SharedMechSystem.cs`:
- Around line 67-69: The attack/weapon event subscriptions on
VehicleOperatorComponent are too broad for SharedMechSystem and should be split
by responsibility. Move the generic cancel logic for AttackAttemptEvent and
CanAttackFromContainerEvent into VehicleSystem, keep only the mech-specific
GetMeleeWeaponEvent fallback in SharedMechSystem, and preserve the existing
TryComp<MechComponent>/_pilotableMechQuery checks while relocating the handlers
OnGetMeleeWeapon, OnCanAttackFromContainer, and OnAttackAttempt to the
appropriate system.

In `@Content.Shared/Vehicle/VehicleSystem.Key.cs`:
- Around line 28-48: `OnGenericKeyedEntInserted` and `OnGenericKeyedEntRemoved`
in `VehicleSystem.Key` duplicate the same state checks, container ID guard,
vehicle lookup, and `RefreshCanRun` call; extract this shared flow into a
private helper and have both handlers delegate to it. Keep the helper centered
around the existing `Entity<GenericKeyedVehicleComponent>` and `RefreshCanRun`
path so the behavior remains identical while removing the repeated logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0da1a7a4-7410-483a-9a19-78c5502c2982

📥 Commits

Reviewing files that changed from the base of the PR and between 56de3e3 and 84fa3a2.

⛔ Files ignored due to path filters (5)
  • Resources/Textures/Objects/Vehicles/janicart.rsi/vehicle-moving.png is excluded by !**/*.png
  • Resources/Textures/Objects/Vehicles/janicart.rsi/vehicle.png is excluded by !**/*.png
  • Resources/Textures/Objects/Vehicles/wheelchair.rsi/wheelchair.png is excluded by !**/*.png
  • Resources/Textures/Objects/Vehicles/wheelchair.rsi/wheelchair_folded.png is excluded by !**/*.png
  • Resources/Textures/Objects/Vehicles/wheelchair.rsi/wheelchair_overlay.png is excluded by !**/*.png
📒 Files selected for processing (59)
  • Content.Client/Backmen/VovaMech/BkmVovaMechHandsUIController.cs
  • Content.Client/Backmen/VovaMech/BkmVovaMechSystem.cs
  • Content.Client/Buckle/BuckleSystem.cs
  • Content.Client/CardboardBox/CardboardBoxSystem.cs
  • Content.Client/MainMenu/UI/MainMenuControl.xaml
  • Content.Client/MainMenu/UI/MainMenuControl.xaml.cs
  • Content.Client/Options/UI/EscapeMenu.xaml
  • Content.Client/UserInterface/Systems/EscapeMenu/EscapeUIController.cs
  • Content.Client/UserInterface/Systems/Hotbar/Widgets/HotbarGui.xaml
  • Content.Client/Weapons/Ranged/Systems/GunSystem.cs
  • Content.Server/Backmen/VovaMech/BkmVovaMechSystem.cs
  • Content.Server/CardboardBox/CardboardBoxSystem.cs
  • Content.Server/Mech/Equipment/EntitySystems/MechGrabberSystem.cs
  • Content.Server/Mech/Systems/MechEquipmentSystem.cs
  • Content.Server/Mech/Systems/MechSystem.cs
  • Content.Server/Physics/Controllers/MoverController.cs
  • Content.Server/Weapons/Ranged/Systems/GunSystem.cs
  • Content.Shared/ActionBlocker/ActionBlockerSystem.cs
  • Content.Shared/Backmen/VovaMech/BkmPilotableMechComponent.cs
  • Content.Shared/Backmen/VovaMech/GetGunHandsHolderEvent.cs
  • Content.Shared/Backmen/VovaMech/SharedBkmVovaMechSystem.cs
  • Content.Shared/Buckle/Components/StrapComponent.cs
  • Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs
  • Content.Shared/CardboardBox/Components/CardboardBoxComponent.cs
  • Content.Shared/DoAfter/DoAfter.cs
  • Content.Shared/DoAfter/SharedDoAfterSystem.Update.cs
  • Content.Shared/DoAfter/SharedDoAfterSystem.cs
  • Content.Shared/Mech/Components/MechComponent.cs
  • Content.Shared/Mech/Components/MechPilotComponent.cs
  • Content.Shared/Mech/EntitySystems/SharedMechSystem.Relay.cs
  • Content.Shared/Mech/EntitySystems/SharedMechSystem.cs
  • Content.Shared/Movement/Components/InputMoverComponent.cs
  • Content.Shared/Movement/Events/UpdateCanMoveEvent.cs
  • Content.Shared/Movement/Systems/SharedMoverController.Input.cs
  • Content.Shared/Movement/Systems/SharedMoverController.Relay.cs
  • Content.Shared/Movement/Systems/SharedMoverController.cs
  • Content.Shared/Vehicle/Components/ContainerVehicleComponent.cs
  • Content.Shared/Vehicle/Components/GenericKeyedVehicleComponent.cs
  • Content.Shared/Vehicle/Components/StrapVehicleComponent.cs
  • Content.Shared/Vehicle/Components/VehicleComponent.cs
  • Content.Shared/Vehicle/Components/VehicleOperatorComponent.cs
  • Content.Shared/Vehicle/VehicleSystem.Key.cs
  • Content.Shared/Vehicle/VehicleSystem.Operator.cs
  • Content.Shared/Vehicle/VehicleSystem.cs
  • Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs
  • Resources/Locale/en-US/_backmen/vova-mech/vova-mech.ftl
  • Resources/Locale/en-US/escape-menu/ui/escape-menu.ftl
  • Resources/Locale/en-US/vehicle/vehicle.ftl
  • Resources/Locale/ru-RU/_backmen/vova-mech/vova-mech.ftl
  • Resources/Locale/ru-RU/escape-menu/ui/escape-menu.ftl
  • Resources/Prototypes/Entities/Objects/Specific/Mech/mechs.yml
  • Resources/Prototypes/Entities/Objects/Vehicles/keys.yml
  • Resources/Prototypes/Entities/Objects/Vehicles/vehicles.yml
  • Resources/Prototypes/Entities/Structures/Storage/Closets/big_boxes.yml
  • Resources/Prototypes/_Backmen/Entities/Mobs/NPCs/mechi_silicon.yml
  • Resources/Prototypes/tags.yml
  • Resources/Textures/Objects/Vehicles/janicart.rsi/meta.json
  • Resources/Textures/Objects/Vehicles/wheelchair.rsi/meta.json
  • Resources/migration.yml
💤 Files with no reviewable changes (5)
  • Content.Shared/Mech/Components/MechPilotComponent.cs
  • Content.Client/MainMenu/UI/MainMenuControl.xaml.cs
  • Content.Shared/Movement/Systems/SharedMoverController.cs
  • Content.Client/MainMenu/UI/MainMenuControl.xaml
  • Content.Shared/Mech/Components/MechComponent.cs

Comment on lines +92 to +112
if (!_entities.TryGetComponent(mech, out HandsComponent? hands))
{
_mechUid = mech;
_mechHands = null;
return;
}

LoadMechHands(mech.Value, hands);
}

private void OnMechHandAdded(Entity<HandsComponent> entity, string name, HandLocation location)
{
if (entity.Owner != _mechUid || _mechHands == null)
return;

if (!_handsSystem.TryGetHand((entity.Owner, entity.Comp), name, out var hand))
return;

AddHandButton(name, hand.Value);
SetActiveHand(_mechHands.ActiveHandId);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n --type=cs -C5 'OnPlayerAddHand' Content.Client/Hands Content.Shared/Hands

Repository: Rxup/space-station-14

Length of output: 2542


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE='Content.Client/Backmen/VovaMech/BkmVovaMechHandsUIController.cs'
wc -l "$FILE"
cat -n "$FILE" | sed -n '1,220p'

printf '\n--- subscriptions/usages ---\n'
rg -n --type=cs -C4 'OnPlayerAddHand|OnPlayerRemoveHand|OnMechHandAdded|OnMechHandRemoved|LoadMechHands|HandsComponent' Content.Client/Backmen/VovaMech Content.Client Content.Shared/Hands

Repository: Rxup/space-station-14

Length of output: 50378


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n --type=cs -C4 'event .*LocalPilotedMechChanged|LocalPilotedMechChanged\+|LocalPilotedMechChanged -|LocalPilotedMechChanged' Content.Client/Backmen/VovaMech Content.Shared Content.Client
printf '\n--- client hands startup section ---\n'
cat -n Content.Client/Hands/Systems/HandsSystem.cs | sed -n '58,110p'
printf '\n--- add hand method section ---\n'
cat -n Content.Shared/Hands/EntitySystems/SharedHandsSystem.cs | sed -n '70,115p'

Repository: Rxup/space-station-14

Length of output: 11926


_mechHands не подхватывается при поздней инициализации рук меха

Если HandsComponent появляется после LocalPilotedMechChanged, _mechHands остаётся null до пересадки: OnMechHandAdded сразу выходит по этому флагу, а других путей повторно вызвать LoadMechHands нет. Добавь восстановление в OnMechHandAdded (или отдельный хук на старт HandsComponent), иначе UI рук меха может так и не появиться в текущей сессии пилотирования.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Content.Client/Backmen/VovaMech/BkmVovaMechHandsUIController.cs` around lines
92 - 112, `_mechHands` can stay null when `HandsComponent` is added after
`LocalPilotedMechChanged`, so the mech hands UI never initializes in the current
session. Update `OnMechHandAdded` in `BkmVovaMechHandsUIController` to recover
from this late-start case by reloading hands when the added component belongs to
`_mechUid` and `_mechHands` is still null, or add a dedicated startup hook for
`HandsComponent` that calls `LoadMechHands`. Keep the existing `AddHandButton`
and `SetActiveHand` flow intact once hands are restored.

Comment on lines +144 to +148
if (!TryInsert(uid, args.User, component))
return;

args.Handled = true;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Нет обратной связи пользователю при неудачной вставке в OnEntry.

Если TryInsert вернёт false (например, слот уже занят другим оператором к моменту завершения DoAfter), метод просто возвращает управление без popup, в отличие от веток mind/CanOperate выше, где пользователь получает "mech-no-enter". Стоит показать аналогичное сообщение и здесь для консистентности UX.

🩹 Предлагаемый фикс
         if (!TryInsert(uid, args.User, component))
+        {
+            _popup.PopupEntity(Loc.GetString("mech-no-enter", ("item", uid)), args.User);
             return;
+        }

         args.Handled = true;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!TryInsert(uid, args.User, component))
return;
args.Handled = true;
}
if (!TryInsert(uid, args.User, component))
{
_popup.PopupEntity(Loc.GetString("mech-no-enter", ("item", uid)), args.User);
return;
}
args.Handled = true;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Content.Server/Backmen/VovaMech/BkmVovaMechSystem.cs` around lines 144 - 148,
The OnEntry flow in BkmVovaMechSystem currently returns silently when
TryInsert(uid, args.User, component) fails, unlike the earlier mind/CanOperate
checks that show "mech-no-enter". Update the TryInsert failure branch to send
the same popup/feedback to the user before returning, keeping the UX consistent
in OnEntry and preserving the existing args.Handled behavior only on success.

Appearance.SetData(buckle, BuckleVisuals.Buckled, false);

if (HasComp<KnockedDownComponent>(buckle) || _mobState.IsCritical(buckle) || _mobState.IsDead(buckle))
if (HasComp<KnockedDownComponent>(buckle) || _mobState.IsIncapacitated(buckle))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the relevant files and inspect the exact implementations/comments.
git ls-files 'Content.Shared/**/MobStateSystem.cs' 'Content.Shared/**/SharedBuckleSystem.Buckle.cs' | sed -n '1,20p'

echo '--- outline MobStateSystem ---'
ast-grep outline Content.Shared/Mobs/Systems/MobStateSystem.cs --view expanded || true

echo '--- outline SharedBuckleSystem.Buckle ---'
ast-grep outline Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs --view expanded || true

echo '--- relevant lines in MobStateSystem ---'
rg -n "IsCritical|IsIncapacitated|SoftCritical|Critical|Dead" Content.Shared/Mobs/Systems/MobStateSystem.cs

echo '--- relevant lines in SharedBuckleSystem.Buckle.cs ---'
rg -n "IsIncapacitated|IsCritical|IsDead|KnockedDownComponent|Stand" Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs

Repository: Rxup/space-station-14

Length of output: 2229


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the relevant implementation snippets with line numbers.
sed -n '1,260p' Content.Shared/Mobs/Systems/MobStateSystem.cs | cat -n | sed -n '1,260p'
echo '---'
sed -n '440,520p' Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs | cat -n | sed -n '1,120p'

Repository: Rxup/space-station-14

Length of output: 9658


IsIncapacitated теряет SoftCritical.
Она охватывает только hard crit/dead, а прежняя проверка IsCritical || IsDead включала и SoftCritical. Из-за этого при отвязке soft-crit сущности будут вставать вместо того, чтобы оставаться лежащими.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs` at line 483, The buckle
release check in SharedBuckleSystem.Buckle should preserve SoftCritical
behavior, since _mobState.IsIncapacitated(buckle) only covers hard crit/dead and
drops soft crit. Update the condition around HasComp<KnockedDownComponent> and
the incapacitation check so it matches the previous IsCritical || IsDead
semantics, keeping SoftCritical entities lying down when unbuckled.

Comment on lines +113 to +189
public bool TrySetOperator(Entity<VehicleComponent> entity, EntityUid? uid, bool removeExisting = true)
{
// Early exit if no change needed
if (entity.Comp.Operator == null && uid is null)
return false;

// Early exit if setting the same operator that's already present
if (entity.Comp.Operator == uid)
return true;

// Do not run logic if the entity is already operating a vehicle.
// However, if they are operating *this* vehicle, return true (they are indeed the operator)
if (uid is not null && _operatorQuery.TryComp(uid, out var eOperator))
{
if (eOperator.Vehicle == entity.Owner)
return true;

// If trying to operate another vehicle, fail unless removeExisting is true
if (!removeExisting)
return false;
}

if (!removeExisting && entity.Comp.Operator is not null)
return false;

if (uid != null && !CanOperate(entity.AsNullable(), uid.Value))
return false;

var oldOperator = entity.Comp.Operator;

if (oldOperator is { } currentOperator &&
_operatorQuery.TryComp(currentOperator, out var currentOperatorComponent))
{
var exitEvent = new OnVehicleExitedEvent(entity, currentOperator);
RaiseLocalEvent(currentOperator, ref exitEvent);

currentOperatorComponent.Vehicle = null;
RemCompDeferred<VehicleOperatorComponent>(currentOperator);
RemCompDeferred<RelayInputMoverComponent>(currentOperator);
RemCompDeferred<InteractionRelayComponent>(currentOperator);
}

entity.Comp.Operator = uid;

if (uid != null)
{
if (_operatorQuery.HasComp(uid.Value))
{
var vehicleOperator = Comp<VehicleOperatorComponent>(uid.Value);
vehicleOperator.Vehicle = entity.Owner;
Dirty(uid.Value, vehicleOperator);
}
else
{
var vehicleOperator = AddComp<VehicleOperatorComponent>(uid.Value);
vehicleOperator.Vehicle = entity.Owner;
Dirty(uid.Value, vehicleOperator);
}

_mover.SetRelay(uid.Value, entity);

var enterEvent = new OnVehicleEnteredEvent(entity, uid.Value);
RaiseLocalEvent(uid.Value, ref enterEvent);
}
else
{
RemCompDeferred<MovementRelayTargetComponent>(entity);
}

RefreshCanRun((entity, entity.Comp));

var setEvent = new VehicleOperatorSetEvent(uid, oldOperator);
RaiseLocalEvent(entity, ref setEvent);

Dirty(entity);
return true;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== VehicleSystem outline ==\n'
ast-grep outline Content.Shared/Vehicle/VehicleSystem.cs --view expanded || true

printf '\n== TrySetOperator / TryRemoveOperator / operator-related symbols ==\n'
rg -n "TrySetOperator|TryRemoveOperator|VehicleOperatorSetEvent|OnVehicleExitedEvent|OnVehicleEnteredEvent|MovementRelayTargetComponent|RefreshCanRun|UpdateAppearance|HasOperator|CanRun" Content.Shared Content.Server Content.Client -S

printf '\n== Call sites that pass removeExisting: true or use TrySetOperator ==\n'
rg -n "TrySetOperator\s*\(" -S .

Repository: Rxup/space-station-14

Length of output: 42592


🏁 Script executed:

#!/bin/bash
set -euo pipefail

for f in \
  Content.Shared/Vehicle/VehicleSystem.cs \
  Content.Shared/Vehicle/VehicleSystem.Operator.cs \
  Content.Shared/Mech/EntitySystems/SharedMechSystem.cs \
  Content.Server/Backmen/VovaMech/BkmVovaMechSystem.cs \
  Content.Shared/Vehicle/Components/VehicleComponent.cs
do
  echo "== $f =="
  wc -l "$f"
done

echo
echo "== VehicleSystem.cs relevant slice =="
sed -n '1,340p' Content.Shared/Vehicle/VehicleSystem.cs | cat -n | sed -n '1,340p'

echo
echo "== VehicleSystem.Operator.cs =="
sed -n '1,220p' Content.Shared/Vehicle/VehicleSystem.Operator.cs | cat -n

echo
echo "== SharedMechSystem.cs relevant slice =="
sed -n '1,520p' Content.Shared/Mech/EntitySystems/SharedMechSystem.cs | cat -n | sed -n '1,520p'

echo
echo "== BkmVovaMechSystem.cs relevant slice =="
sed -n '1,240p' Content.Server/Backmen/VovaMech/BkmVovaMechSystem.cs | cat -n | sed -n '1,240p'

echo
echo "== VehicleComponent.cs =="
sed -n '1,160p' Content.Shared/Vehicle/Components/VehicleComponent.cs | cat -n

Repository: Rxup/space-station-14

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
path = Path('Content.Shared/Vehicle/VehicleSystem.cs')
text = path.read_text()
for needle in ['public bool TrySetOperator', 'public bool TryRemoveOperator', 'public bool HasOperator', 'public void RefreshCanRun', 'private void UpdateAppearance']:
    idx = text.find(needle)
    print(f"\n=== {needle} @ {idx} ===")
    if idx != -1:
        start = max(0, text.rfind('\n', 0, idx - 200))
        end = min(len(text), idx + 1400)
        snippet = text[start:end]
        for i, line in enumerate(snippet.splitlines(), 1):
            print(f"{i:>4}: {line}")
PY

Repository: Rxup/space-station-14

Length of output: 8191


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== VehicleOperatorComponent =='
sed -n '1,200p' Content.Shared/Vehicle/Components/VehicleOperatorComponent.cs | cat -n

echo
echo '== References to VehicleOperatorShutdownEvent / VehicleOperatorSetEvent handlers =='
rg -n "VehicleOperatorShutdownEvent|VehicleOperatorSetEvent" Content.Shared Content.Server Content.Client -S

echo
echo '== References to VehicleOperatorComponent.Vehicle assignments or reads =='
rg -n "\.Vehicle\b" Content.Shared Content.Server Content.Client -S | rg "VehicleOperatorComponent|VehicleSystem|OnOperatorSet|TrySetOperator|TryRemoveOperator|VehicleOperatorShutdownEvent|VehicleOperatorSetEvent" -n -S

Repository: Rxup/space-station-14

Length of output: 4379


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
path = Path('Content.Shared/Vehicle/Components/VehicleOperatorComponent.cs')
print(path.read_text())
PY

Repository: Rxup/space-station-14

Length of output: 753


При removeExisting == true нужно очищать старый транспорт оператора. Если uid уже был оператором другого VehicleComponent, текущий код перепривязывает VehicleOperatorComponent.Vehicle, но не снимает его со старого транспорта, и там остаётся устаревший Operator/HasOperator/CanRun.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Content.Shared/Vehicle/VehicleSystem.cs` around lines 113 - 189, In
TrySetOperator, when removeExisting is true and uid already has a
VehicleOperatorComponent for another vehicle, clear that previous vehicle’s
operator state before reassigning. Update VehicleSystem.TrySetOperator so it
removes the old operator from the previous VehicleComponent, refreshes that
vehicle’s state (including operator/can-run related flags), and only then
assigns uid to the new entity. Use the existing operator handling paths around
_operatorQuery, VehicleOperatorComponent.Vehicle, and RefreshCanRun to keep both
vehicles consistent.

Comment on lines +18 to +30
private void OnVehicleStrapped(Entity<StrapVehicleComponent> ent, ref StrappedEvent args)
{
if (!_vehicleQuery.TryComp(ent, out var vehicle))
return;
TrySetOperator((ent, vehicle), args.Buckle);
}

private void OnVehicleUnstrapped(Entity<StrapVehicleComponent> ent, ref UnstrappedEvent args)
{
if (!_vehicleQuery.TryComp(ent, out var vehicle))
return;
TrySetOperator((ent, vehicle), null);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Отсутствует проверка _timing.ApplyingState в отличие от остальных обработчиков.

OnVehicleStrapped/OnVehicleUnstrapped не проверяют _timing.ApplyingState, тогда как аналогичные обработчики в этом же файле (OnContainerEntInserted/OnContainerEntRemoved, Line 34, 45) и в VehicleSystem.Key.cs (OnGenericKeyedInsertAttempt, OnGenericKeyedEntInserted, OnGenericKeyedEntRemoved) явно игнорируют логику во время применения состояния клиентом. Это несогласованность паттерна: при применении сетевого состояния на клиенте TrySetOperator может выполниться повторно (например, повторно поднять OnVehicleEnteredEvent/OnVehicleExitedEvent, снова вызвать _mover.SetRelay), что может приводить к лишним побочным эффектам или рассинхронизации предсказания.

Прошу подтвердить, действительно ли StrappedEvent/UnstrappedEvent не поднимаются во время применения состояния (в отличие от контейнерных событий), иначе стоит добавить такую же защиту.

♻️ Возможный фикс для согласованности
     private void OnVehicleStrapped(Entity<StrapVehicleComponent> ent, ref StrappedEvent args)
     {
+        if (_timing.ApplyingState)
+            return;
+
         if (!_vehicleQuery.TryComp(ent, out var vehicle))
             return;
         TrySetOperator((ent, vehicle), args.Buckle);
     }

     private void OnVehicleUnstrapped(Entity<StrapVehicleComponent> ent, ref UnstrappedEvent args)
     {
+        if (_timing.ApplyingState)
+            return;
+
         if (!_vehicleQuery.TryComp(ent, out var vehicle))
             return;
         TrySetOperator((ent, vehicle), null);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private void OnVehicleStrapped(Entity<StrapVehicleComponent> ent, ref StrappedEvent args)
{
if (!_vehicleQuery.TryComp(ent, out var vehicle))
return;
TrySetOperator((ent, vehicle), args.Buckle);
}
private void OnVehicleUnstrapped(Entity<StrapVehicleComponent> ent, ref UnstrappedEvent args)
{
if (!_vehicleQuery.TryComp(ent, out var vehicle))
return;
TrySetOperator((ent, vehicle), null);
}
private void OnVehicleStrapped(Entity<StrapVehicleComponent> ent, ref StrappedEvent args)
{
if (_timing.ApplyingState)
return;
if (!_vehicleQuery.TryComp(ent, out var vehicle))
return;
TrySetOperator((ent, vehicle), args.Buckle);
}
private void OnVehicleUnstrapped(Entity<StrapVehicleComponent> ent, ref UnstrappedEvent args)
{
if (_timing.ApplyingState)
return;
if (!_vehicleQuery.TryComp(ent, out var vehicle))
return;
TrySetOperator((ent, vehicle), null);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Content.Shared/Vehicle/VehicleSystem.Operator.cs` around lines 18 - 30,
Добавьте проверку _timing.ApplyingState в обработчики OnVehicleStrapped и
OnVehicleUnstrapped, чтобы они вели себя согласованно с
OnContainerEntInserted/OnContainerEntRemoved и обработчиками в
VehicleSystem.Key.cs. Сейчас эти события сразу вызывают TrySetOperator, что
может повторно срабатывать при применении клиентского состояния; перед вызовом
TrySetOperator просто выходите из обработчика, если _timing.ApplyingState
истинно.

Comment on lines 232 to 248
maxEquipmentAmount: 2
airtight: true
equipmentWhitelist:
tags:
- SmallMech
pilotWhitelist:
- type: Vehicle
transferDamageModifier:
coefficients:
Blunt: 0.2
Slash: 0.2
Piercing: 0.2
Cold: 0.2
Heat: 0.2
Shock: 0.2
operatorWhitelist:
tags:
- Hamster

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Ищем мобов с тегами Hamster / VimPilot и проверяем наличие компонента Hands
rg -n -B3 -A15 'tags:\s*\n\s*-\s*Hamster' -g '*.yml' -P
rg -n -B3 -A15 'tags:\s*\n\s*-\s*VimPilot' -g '*.yml' -P

Repository: Rxup/space-station-14

Length of output: 159


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== mechs.yml relevant sections =="
sed -n '220,260p' Resources/Prototypes/Entities/Objects/Specific/Mech/mechs.yml
echo
sed -n '300,330p' Resources/Prototypes/Entities/Objects/Specific/Mech/mechs.yml
echo
echo "== requiresHands in mechs.yml =="
rg -n 'requiresHands|operatorWhitelist|MechHamtr|MechVim' Resources/Prototypes/Entities/Objects/Specific/Mech/mechs.yml

echo
echo "== HandsComponent / hand-related presence for likely operators =="
rg -n 'Hamster|VimPilot|HandsComponent|hands component|Hands' Resources/Prototypes -g '*.yml'

Repository: Rxup/space-station-14

Length of output: 50378


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== tags.yml references =="
rg -n -A4 -B4 'Hamster|VimPilot' Resources/Prototypes -g '*.yml'

echo
echo "== component definitions with hands =="
rg -n 'HandsComponent|hands' Content.* Resources -g '*.yml' -g '*.cs' | head -n 200

Repository: Rxup/space-station-14

Length of output: 50378


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== MobHamster definition =="
sed -n '3438,3495p' Resources/Prototypes/Entities/Mobs/NPCs/animals.yml

echo
echo "== mouse / VimPilot-related definition =="
sed -n '1,120p' Resources/Prototypes/_Backmen/Entities/Mobs/Species/mouse.yml

echo
echo "== other VimPilot mobs with Hands component nearby =="
rg -n -A6 -B6 '^\s*- type: Hands\b|^\s*- type: Tag\b|VimPilot|Hamster\b' Resources/Prototypes/Entities/Mobs/NPCs/pets.yml Resources/Prototypes/Entities/Mobs/NPCs/animals.yml Resources/Prototypes/_Backmen/Entities/Mobs/Species/mouse.yml -g '*.yml'

Repository: Rxup/space-station-14

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== VehicleComponent and VehicleSystem references to requiresHands =="
rg -n -A4 -B4 'requiresHands|CanOperate|HandsComponent' Content.* Resources -g '*.cs' -g '*.yml' | head -n 120

Repository: Rxup/space-station-14

Length of output: 10512


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

path = Path("Resources/Prototypes/Entities/Mobs/NPCs/animals.yml")
lines = path.read_text().splitlines()

def extract_entity_block(entity_id):
    start = None
    for i, line in enumerate(lines):
        if line.strip() == f"id: {entity_id}":
            start = i
            break
    if start is None:
        return None
    end = len(lines)
    for j in range(start + 1, len(lines)):
        if lines[j].startswith("- type: entity"):
            end = j
            break
    return start, end, lines[start:end]

for entity_id in ["MobHamster", "MobHamsterHamlet"]:
    res = extract_entity_block(entity_id)
    if not res:
        print(f"{entity_id}: NOT FOUND")
        continue
    start, end, block = res
    has_hands = any(l.strip() == "- type: Hands" for l in block)
    tags = []
    for idx, l in enumerate(block):
        if l.strip() == "- type: Tag":
            for k in range(idx + 1, min(idx + 12, len(block))):
                if block[k].strip() == "tags:":
                    for m in range(k + 1, min(k + 10, len(block))):
                        s = block[m].strip()
                        if s.startswith("- "):
                            tags.append(s[2:])
                    break
    print(f"{entity_id}: lines {start+1}-{end}")
    print(f"  HandsComponent: {has_hands}")
    print(f"  Tags: {tags}")
    print()

# Check operator whitelist targets in mechs.yml
mechs = Path("Resources/Prototypes/Entities/Objects/Specific/Mech/mechs.yml").read_text().splitlines()
for mech_id in ["MechHamtr", "MechVim"]:
    start = None
    for i, line in enumerate(mechs):
        if line.strip() == f"id: {mech_id}":
            start = i
            break
    if start is None:
        print(f"{mech_id}: NOT FOUND")
        continue
    end = len(mechs)
    for j in range(start + 1, len(mechs)):
        if mechs[j].startswith("- type: entity"):
            end = j
            break
    block = mechs[start:end]
    req = [l.strip() for l in block if "requiresHands" in l]
    wl = []
    capture = False
    for l in block:
        if l.strip().startswith("operatorWhitelist:"):
            capture = True
            continue
        if capture and l.startswith("  - type:"):
            break
        if capture and l.strip().startswith("- "):
            wl.append(l.strip()[2:])
    print(f"{mech_id}: lines {start+1}-{end}")
    print(f"  requiresHands lines: {req if req else 'none (default applies)'}")
    print(f"  operatorWhitelist tags: {wl}")
    print()
PY

Repository: Rxup/space-station-14

Length of output: 541


Явно отключить requiresHands у MechHamtrMobHamster не имеет HandsComponent, а VehicleSystem.CanOperate по умолчанию требует руки, так что хомяки не смогут пилотировать этот мех.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Resources/Prototypes/Entities/Objects/Specific/Mech/mechs.yml` around lines
232 - 248, `MechHamtr` still relies on the default `VehicleSystem.CanOperate`
hand requirement, which blocks `MobHamster` from piloting because it has no
`HandsComponent`. Update the `MechHamtr` entity definition in the mech YAML to
explicitly set `requiresHands` to false on the `Vehicle` component, keeping the
existing `operatorWhitelist` for `Hamster` intact so the mech can be operated
without hands.

@Rxup
Rxup merged commit c712a37 into master Jul 3, 2026
14 of 15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Changes: Localization Can be reviewed or fixed by people who are knowledgeable with translation Changes: Sprites Should be reviewed or fixed by people who are knowledgeable with spriting or visual design. Changes: UI Can be reviewed or fixed by people who are knowledgeable with UI design size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant